import { get, initial, isEmpty, join, last, mapValues, size, slice, sortBy, split, toUpper } from 'lodash'
import useConnectionStore from 'stores/connections.js'
import { ConnectionType } from '@/consts/connection_type.js'
import { RedisDatabaseItem } from '@/objects/redisDatabaseItem.js'
import { KeyViewType } from '@/consts/key_view_type.js'
import { RedisNodeItem } from '@/objects/redisNodeItem.js'
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'

/**
 * server connection state
 */
export class RedisServerState {
  /**
   * @typedef {Object} LoadingState
   * @property {boolean} loading indicated that is loading children now
   * @property {boolean} fullLoaded indicated that all children already loaded
   */

  /**
   * @param {string} name server name
   * @param {number} db current opened database
   * @param {number} reloadKey try to reload when changed
   * @param {{}} stats current server status info
   * @param {Object.<number, RedisDatabaseItem>} databases database list
   * @param {string|null} patternFilter pattern filter
   * @param {string|null} typeFilter redis type filter
   * @param {boolean} exactFilter exact match filter keyword
   * @param {LoadingState} loadingState all loading state in opened connections map by server and LoadingState
   * @param {KeyViewType} viewType view type selection for all opened connections group by 'server'
   * @param {Map<string, RedisNodeItem>} nodeMap map nodes by "type#key"
   */
  constructor({
    name,
    db = 0,
    stats = {},
    databases = {},
    patternFilter = null,
    typeFilter = null,
    exactFilter = false,
    loadingState = {},
    viewType = KeyViewType.Tree,
    nodeMap = new Map(),
  }) {
    this.name = name
    this.db = db
    this.reloadKey = Date.now()
    this.stats = stats
    this.databases = databases
    this.patternFilter = patternFilter
    this.typeFilter = typeFilter
    this.exactFilter = exactFilter
    this.loadingState = loadingState
    this.viewType = viewType
    this.nodeMap = nodeMap
    this.decodeHistory = new Map()
    this.decodeHistoryLimit = 100
    this.getRoot()

    const connStore = useConnectionStore()
    const keySeparator = connStore.getDefaultSeparator(name)
    this.separator = isEmpty(keySeparator) ? ':' : keySeparator
  }

  dispose() {
    this.stats = {}
    this.patternFilter = null
    this.typeFilter = null
    this.exactFilter = false
    this.nodeMap.clear()
  }

  closeDatabase() {
    this.patternFilter = null
    this.typeFilter = null
    this.nodeMap.clear()
  }

  setDatabaseKeyCount(db, maxKeys) {
    const dbInst = this.databases[db]
    if (dbInst == null) {
      this.databases[db] = new RedisDatabaseItem({ db, maxKeys })
    } else {
      dbInst.maxKeys = maxKeys
    }
    return dbInst
  }

  /**
   * update max key by increase/decrease value
   * @param {number} db
   * @param {number} updateVal
   */
  updateDBKeyCount(db, updateVal) {
    const dbInst = this.databases[db]
    if (dbInst != null) {
      dbInst.maxKeys = Math.max(0, dbInst.maxKeys + updateVal)
    }
  }

  /**
   * set db max keys value
   * @param {number} db
   * @param {number} count
   */
  setDBKeyCount(db, count) {
    const dbInst = this.databases[db]
    if (dbInst != null) {
      dbInst.maxKeys = Math.max(0, count)
    }
  }

  /**
   * get tree root item
   * @returns {RedisNodeItem}
   */
  getRoot() {
    const rootKey = `${ConnectionType.RedisDB}`
    let root = this.nodeMap.get(rootKey)
    if (root == null) {
      // create root node
      root = new RedisNodeItem({
        key: rootKey,
        label: `db${this.db}`,
        type: ConnectionType.RedisDB,
        children: [],
      })
      this.nodeMap.set(rootKey, root)
    }
    return root
  }

  /**
   * get database list sort by db asc
   * @return {RedisDatabaseItem[]}
   */
  getDatabase() {
    return sortBy(mapValues(this.databases), 'db')
  }

  /**
   *
   * @param {ConnectionType} type
   * @param {string} keyPath
   * @param {RedisNodeItem} node
   */
  addNode(type, keyPath, node) {
    this.nodeMap.set(`${type}/${keyPath}`, node)
  }

  /**
   * add keys to current opened database
   * @param {Array<string|number[]>|Set<string|number[]>} keys
   * @param {boolean} [sortInsert]
   * @return {{newKey: number, newLayer: number, success: boolean, replaceKey: number}}
   */
  addKeyNodes(keys, sortInsert) {
    const result = {
      success: false,
      newLayer: 0,
      newKey: 0,
      replaceKey: 0,
    }
    const root = this.getRoot()

    if (this.viewType === KeyViewType.List) {
      // construct list view data
      for (const key of keys) {
        const k = decodeRedisKey(key)
        const isBinaryKey = k !== key
        const nodeKey = `${ConnectionType.RedisValue}/${nativeRedisKey(key)}`
        const replaceKey = this.nodeMap.has(nodeKey)
        const selectedNode = new RedisNodeItem({
          key: `${this.name}/db${this.db}#${nodeKey}`,
          label: k,
          db: this.db,
          keyCount: 0,
          redisKey: k,
          redisKeyCode: isBinaryKey ? key : undefined,
          redisKeyType: undefined,
          type: ConnectionType.RedisValue,
          isLeaf: true,
        })
        this.nodeMap.set(nodeKey, selectedNode)
        if (!replaceKey) {
          root.addChild(selectedNode, sortInsert)
          result.newKey += 1
        } else {
          result.replaceKey += 1
        }
      }
    } else {
      // construct tree view data
      for (const key of keys) {
        const k = decodeRedisKey(key)
        const isBinaryKey = k !== key
        const keyParts = isBinaryKey ? [nativeRedisKey(key)] : split(k, this.separator)
        const len = size(keyParts)
        const lastIdx = len - 1
        let handlePath = ''
        let node = root
        for (let i = 0; i < len; i++) {
          handlePath += keyParts[i]
          if (i !== lastIdx) {
            // layer
            const nodeKey = `${ConnectionType.RedisKey}/${handlePath}`
            let selectedNode = this.nodeMap.get(nodeKey)
            if (selectedNode == null) {
              selectedNode = new RedisNodeItem({
                key: `${this.name}/db${this.db}#${nodeKey}`,
                label: keyParts[i],
                db: this.db,
                keyCount: 0,
                redisKey: handlePath,
                type: ConnectionType.RedisKey,
                isLeaf: false,
                children: [],
              })
              this.nodeMap.set(nodeKey, selectedNode)
              node.addChild(selectedNode, sortInsert)
              result.newLayer += 1
            }
            node = selectedNode
            handlePath += this.separator
          } else {
            // key
            const nodeKey = `${ConnectionType.RedisValue}/${handlePath}`
            const replaceKey = this.nodeMap.has(nodeKey)
            const selectedNode = new RedisNodeItem({
              key: `${this.name}/db${this.db}#${nodeKey}`,
              label: isBinaryKey ? k : keyParts[i],
              db: this.db,
              keyCount: 0,
              redisKey: handlePath,
              redisKeyCode: isBinaryKey ? key : undefined,
              redisKeyType: undefined,
              type: ConnectionType.RedisValue,
              isLeaf: true,
            })
            this.nodeMap.set(nodeKey, selectedNode)
            if (!replaceKey) {
              node.addChild(selectedNode, sortInsert)
              result.newKey += 1
            } else {
              result.replaceKey += 1
            }
          }
        }
      }
    }
    return result
  }

  /**
   * rename key to a new name
   * @param {string} key
   * @param {string} newKey
   */
  renameKey(key, newKey) {
    const oldLayer = initial(key.split(this.separator)).join(this.separator)
    const newLayer = initial(newKey.split(this.separator)).join(this.separator)
    if (oldLayer !== newLayer) {
      // also change layer
      this.removeKeyNode(key, false)
      const { success } = this.addKeyNodes([newKey], true)
      if (success) {
        this.tidyNode(newLayer)
      }
    } else {
      // change key name only
      const oldNodeKeyName = `${ConnectionType.RedisValue}/${key}`
      const newNodeKeyName = `${ConnectionType.RedisValue}/${newKey}`
      const keyNode = this.nodeMap.get(oldNodeKeyName)
      keyNode.key = `${this.name}/db${this.db}#${newNodeKeyName}`
      if (this.viewType === KeyViewType.Tree) {
        keyNode.label = last(split(newKey, this.separator))
      } else {
        keyNode.label = newKey
      }
      keyNode.redisKey = newKey
      // not support rename binary key name yet
      // keyNode.redisKeyCode = []
      this.nodeMap.set(newNodeKeyName, keyNode)
      this.nodeMap.delete(oldNodeKeyName)
    }
  }

  /**
   * remove key node by key name
   * @param {string} [key]
   * @param {boolean} [isLayer]
   * @return {boolean}
   */
  removeKeyNode(key, isLayer) {
    if (isLayer === true) {
      this.deleteChildrenKeyNodes(key)
    }

    const dbRoot = this.getRoot()
    if (isEmpty(key)) {
      // clear all key nodes
      this.nodeMap.clear()
      this.getRoot()
      const dbInst = this.databases[this.db]
      if (dbInst != null) {
        dbInst.maxKeys = 0
        dbInst.keyCount = 0
      }
    } else {
      const keyParts = split(key, this.separator)
      const totalParts = size(keyParts)
      // remove from parent in tree node
      const parentKey = slice(keyParts, 0, totalParts - 1)
      let parentNode
      if (isEmpty(parentKey)) {
        parentNode = dbRoot
      } else {
        parentNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, this.separator)}`)
      }

      // not found parent node
      if (parentNode == null) {
        return false
      }
      parentNode.removeChild({
        type: isLayer ? ConnectionType.RedisKey : ConnectionType.RedisValue,
        redisKey: key,
      })

      // // check and remove empty layer node
      // let i = totalParts - 1
      // for (; i >= 0; i--) {
      //     const anceKey = join(slice(keyParts, 0, i), this.separator)
      //     if (i > 0) {
      //         const anceNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`)
      //         const redisKey = join(slice(keyParts, 0, i + 1), this.separator)
      //         anceNode.removeChild({ type: ConnectionType.RedisKey, redisKey })
      //
      //         if (isEmpty(anceNode.children)) {
      //             this.nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`)
      //         } else {
      //             break
      //         }
      //     } else {
      //         // last one, remove from db node
      //         dbRoot.removeChild({ type: ConnectionType.RedisKey, redisKey: keyParts[0] })
      //         this.nodeMap.delete(`${ConnectionType.RedisValue}/${keyParts[0]}`)
      //     }
      // }
    }

    return true
  }

  /**
   * tidy node by key
   * @param {string} [key]
   * @param {boolean} [skipResort]
   * @return
   */
  tidyNode(key, skipResort) {
    const rootNode = this.getRoot()
    const keyParts = split(key, this.separator)
    const totalParts = size(keyParts)
    let node
    // find last exists ancestor key
    let i = totalParts - 1
    for (; i > 0; i--) {
      const parentKey = join(slice(keyParts, 0, i), this.separator)
      node = this.nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
      if (node != null) {
        break
      }
    }
    if (node == null) {
      node = rootNode
    }
    const keyCountUpdated = node.tidy(skipResort)
    if (keyCountUpdated) {
      // update key count of parent and above
      for (; i > 0; i--) {
        const parentKey = join(slice(keyParts, 0, i), this.separator)
        const parentNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
        if (parentNode == null) {
          break
        }
        const count = parentNode.reCalcKeyCount()
        if (count <= 0) {
          let anceKeyNode = rootNode
          // remove from ancestor node
          if (i > 1) {
            const anceKey = join(slice(keyParts, 0, i - 1), this.separator)
            anceKeyNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`)
          }
          if (anceKeyNode != null) {
            anceKeyNode.removeChild({ type: ConnectionType.RedisKey, redisKey: parentKey })
          }
        }
      }
      // update key count of db
      const dbInst = this.databases[this.db]
      if (dbInst != null) {
        dbInst.keyCount = rootNode.reCalcKeyCount()
      }
    }
  }

  /**
   * add keys to current opened database
   * @param {ConnectionType} type
   * @param {string} keyPath
   * @return {RedisNodeItem|null}
   */
  getNode(type, keyPath) {
    return this.nodeMap.get(`${type}/${keyPath}`) || null
  }

  /**
   * delete node and all it's children from nodeMap
   * @param {string} [key] clean nodeMap if key is empty
   * @private
   */
  deleteChildrenKeyNodes(key) {
    if (isEmpty(key)) {
      this.nodeMap.clear()
      this.getRoot()
    } else {
      const nodeKey = `${ConnectionType.RedisKey}/${key}`
      const node = this.nodeMap.get(nodeKey)
      const children = node.children || []
      for (const child of children) {
        if (child.type === ConnectionType.RedisValue) {
          if (!this.nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) {
            console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`)
          }
        } else if (child.type === ConnectionType.RedisKey) {
          this.deleteChildrenKeyNodes(child.redisKey)
        }
      }
      if (!this.nodeMap.delete(nodeKey)) {
        console.warn('delete map key', nodeKey)
      }
    }
  }

  getFilter() {
    let pattern = this.patternFilter
    if (isEmpty(pattern)) {
      const conn = useConnectionStore()
      pattern = conn.getDefaultKeyFilter(this.name)
    }
    return {
      match: pattern,
      type: toUpper(this.typeFilter),
      exact: this.exactFilter === true,
    }
  }

  /**
   * set key filter
   * @param {string} [pattern]
   * @param {string} [type]
   * @param {boolean} [exact]
   */
  setFilter({ pattern, type, exact = false }) {
    this.patternFilter = pattern === null ? this.patternFilter : pattern
    this.typeFilter = type === null ? this.typeFilter : type
    this.exactFilter = exact === true
  }

  /**
   * add manually selected decode type to history
   * @param {string} key
   * @param {number} db
   * @param {string} format
   * @param {string} decode
   */
  addDecodeHistory(key, db, format = '', decode = '') {
    const decodeKey = `${key}#${db}`
    this.decodeHistory.delete(decodeKey)
    if (isEmpty(format) && isEmpty(decode)) {
      // reset to default, remove from history
      return
    }

    this.decodeHistory.set(decodeKey, [format, decode])
    while (this.decodeHistory.size > this.decodeHistoryLimit) {
      const k = this.decodeHistory.keys().next().value
      this.decodeHistory.delete(k)
    }
  }

  /**
   * get manually selected decode type from history
   * @param {string|number[]} key
   * @param {number} db
   * @return {[]}
   */
  getDecodeHistory(key, db) {
    const h = this.decodeHistory.get(`${nativeRedisKey(key)}#${db}`) || []
    return [get(h, 0, ''), get(h, 1, '')]
  }
}
